package com.example.springboot.config.aop.Idempotentandantishake;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.example.springboot.config.anotation.idempotentandantishake.Idempotent;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 幂等和防抖的AOP
 *
 * @author chenzhangkun
 * @ClassName IdempotentAndAntiShake
 * @description: 这里我们定义一个切入点，也就是我们定义的注解，然后在调用接口之前获取到接口的入参以及注解的参数.
 * 获取到这些之后，判断是否有唯一标识，没有就用路由，保存到reids当中，然后设置过期时间，
 * 最后需要把删除的标识放到线程私有变量THREAD_CACHE中在接口处理完之后判断是否需要删除redis当中保存的ke
 * @date 2024年12月09日
 */
@Aspect
@Component
@Slf4j
public class IdempotentAndAntiShakeAop {

	@Resource
	private RedisTemplate redisTemplate;
	/**
	 * SPEL解析器
	 */
	private static final SpelExpressionParser PARSER = new SpelExpressionParser();

	private static final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

	private static final ThreadLocal<Map<String, Object>> THREAD_CACHE = ThreadLocal.withInitial(HashMap::new);

	private static final String KEY = "key";

	private static final String DEL_KEY = "delKey";

	@Pointcut("@annotation(com.example.springboot.config.anotation.idempotentandantishake.Idempotent)")
	public void idempotent() {
	}

	@Before("idempotent()")
	public void before(JoinPoint joinPoint) throws Throwable {
		ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		HttpServletRequest request = requestAttributes.getRequest();
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		Method method = signature.getMethod();
		if (!method.isAnnotationPresent(Idempotent.class)) {
			// 没有幂等接口，放过
			return;
		}
		Idempotent idempotent = method.getAnnotation(Idempotent.class);
		String key;
		if (StrUtil.isBlank(idempotent.key())) {
			String url = request.getRequestURL().toString();
			String argString = Arrays.asList(joinPoint.getArgs()).toString();
			key = url + argString;
		} else {
			key = resolver(idempotent, joinPoint);
		}
		//从注解中读取并设置幂等操作的过期时间、描述信息、时间单位以及是否删除键的标志。
		long expireTime = idempotent.expireTime();
		String info = idempotent.info();
		TimeUnit timeUnit = idempotent.timeUnit();
		boolean delKey = idempotent.delKey();
		String value = LocalDateTime.now().toString().replace("T", " ");
		Object valueResult = redisTemplate.opsForValue().get(key);
		synchronized (this) {
			if (null == valueResult) {
				redisTemplate.opsForValue().set(key, value, expireTime, timeUnit);
			} else {
				throw new RuntimeException(info);
			}
		}
		Map<String, Object> map = THREAD_CACHE.get();
		map.put(KEY, key);
		map.put(DEL_KEY, delKey);
	}

	/**
	 * 从注解的方法的参数中解析出用于幂等性处理的键值（key）
	 *
	 * @param idempotent
	 * @param joinPoint
	 * @return
	 */
	private String resolver(Idempotent idempotent, JoinPoint joinPoint) {
		Object[] arguments = joinPoint.getArgs();
		String[] params = DISCOVERER.getParameterNames(getMethod(joinPoint));
		StandardEvaluationContext context = new StandardEvaluationContext();
		if (params != null && params.length > 0) {
			for (int len = 0; len < params.length; len++) {
				context.setVariable(params[len], arguments[len]);
			}
		}
		Expression expression = PARSER.parseExpression(idempotent.key());
		return expression.getValue(context, String.class);
	}

	/**
	 * 根据切点解析方法信息
	 *
	 * @param joinPoint 切点信息
	 * @return Method    原信息
	 */
	private Method getMethod(JoinPoint joinPoint) {
		//将joinPoint.getSignature()转换为MethodSignature
		//Signature是AOP中表示连接点签名的接口，而MethodSignature是它的具体实现，专门用于表示方法的签名。
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		//获取到方法的声明。这将返回代理对象所持有的方法声明。
		Method method = signature.getMethod();

		//判断获取到的方法是否属于一个接口
		//因为在Java中，当通过Spring    AOP或其它代理方式调用接口的方法时，实际被执行的对象是一个代理对象，直接获取到的方法可能来自于接口声明而不是实现类。
		if (method.getDeclaringClass().isInterface()) {
			try {
				//通过反射获取目标对象的实际类（joinPoint.getTarget().getClass()）中同名且参数类型相同的方法
				//这样做是因为代理类可能对方法进行了增强，直接调用实现类的方法可以确保获取到最准确的实现细节
				method = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
						method.getParameterTypes());
			} catch (SecurityException | NoSuchMethodException e) {
				throw new RuntimeException(e);
			}
		}
		return method;
	}

	@After("idempotent()")
	public void after() throws Throwable {
		Map<String, Object> map = THREAD_CACHE.get();
		if (CollUtil.isEmpty(map)) {
			return;
		}

		String key = map.get(KEY).toString();
		boolean delKey = (boolean) map.get(DEL_KEY);
		if (delKey) {
			redisTemplate.delete(KEY);
			log.info("[idempotent]:has removed key={}", key);
		}
		//无论是否移除了键，最后都会清空当前线程局部变量THREAD_CACHE中的数据，避免内存泄漏
		THREAD_CACHE.remove();
	}

}
